/**
 * 
 */
package truerefactor.refactor;

import java.util.ArrayList;
import java.util.List;

import truerefactor.graph.AttributeNode;
import truerefactor.graph.ClassNode;
import truerefactor.graph.CodeGraph;
import truerefactor.graph.CodeNode;
import truerefactor.graph.Edge;
import truerefactor.graph.EdgeType;
import truerefactor.graph.MethodNode;

/**
 * CollapseHierarchy - An implementation of the Collapse Hierarchy Refactoring
 * Technique
 * 
 * @author Isaac Griffith
 */
public class CollapseHierarchy extends Refactoring {

    /**
     * Creates a new instance of CollapseHierarchy
     */
    public CollapseHierarchy()
    {
        this.name = "Collapse Inheritance Hierarchy";
        this.description = "";
    }

    /*
     * (non-Javadoc)
     * @see
     * truerefactor.refactor.Refactoring#execute(truerefactor.graph.CodeNode [])
     */
    /**
     * Expects that the supplied Code Nodes are all of type ClassNode Expects
     * that the first CodeNode is the class to be removed.
     */
    @Override
    public void execute(CodeGraph graph)
    {
        ClassNode superClass = (ClassNode) operands.get(0);
        ClassNode subClass = (ClassNode) operands.get(1);
        
        boolean removeSuper = selectClassToRemove(superClass);

        adjustHierarchyFeatures(graph, superClass, subClass, removeSuper);

        adjustReferences(graph, superClass, subClass, removeSuper);

        removeClasses(graph, superClass, subClass, removeSuper);
    }

    private boolean selectClassToRemove(ClassNode superClass)
    {
        boolean removeSuper;
        // 1. Choose which class is going to be removed: The superclass or the
        // subclass
        if (superClass.getSubClasses().size() > 1)
        {
            removeSuper = false;
        }
        else
        {
            removeSuper = true;
        }
        return removeSuper;
    }

    private void adjustHierarchyFeatures(CodeGraph graph, ClassNode superClass, ClassNode subClass, boolean removeSuper)
    {
        // 2. Use Pull Up Field and Pull Up Method, or Push Down Field and Push
        // Down Method to move all the behavior and data of the removed class to
        // the class with which it is being merged.

        if (removeSuper)
        {
            List<MethodNode> methods = new ArrayList<MethodNode>();
            for (CodeNode node : superClass.getMethods())
            {
                if (node instanceof MethodNode)
                    methods.add((MethodNode) node);
            }

            for (MethodNode mNode : methods)
            {
                CodeNode ops[] = { mNode };
                Refactoring ref = RefactoringFactory.getInstance().createPushDownMethod(ops);
                ref.execute(graph);
            }

            List<AttributeNode> attrs = new ArrayList<AttributeNode>();
            for (CodeNode node : superClass.getAttributes())
            {
                if (node instanceof AttributeNode)
                    attrs.add((AttributeNode) node);
            }

            for (AttributeNode aNode : attrs)
            {
                CodeNode ops[] = { aNode };
                Refactoring ref = RefactoringFactory.getInstance().createPushDownField(ops);
                ref.execute(graph);
            }
        }
        else
        {
            List<MethodNode> methods = new ArrayList<MethodNode>();
            for (CodeNode node : subClass.getMethods())
            {
                if (node instanceof MethodNode)
                    methods.add((MethodNode) node);
            }

            for (MethodNode mNode : methods)
            {
                CodeNode ops[] = { mNode };
                Refactoring ref = RefactoringFactory.getInstance().createPullUpMethod(ops);
                ref.execute(graph);
            }

            List<AttributeNode> attrs = new ArrayList<AttributeNode>();
            for (CodeNode node : subClass.getAttributes())
            {
                if (node instanceof AttributeNode)
                    attrs.add((AttributeNode) node);
            }

            for (AttributeNode aNode : attrs)
            {
                CodeNode ops[] = { aNode };
                Refactoring ref = RefactoringFactory.getInstance().createPullUpField(ops);
                ref.execute(graph);
            }
        }
    }

    private void adjustReferences(CodeGraph graph, ClassNode superClass, ClassNode subClass, boolean removeSuper)
    {
        // 3. Adjust references to the class that will be removed to used the
        // merged class. This will affect variable declarations, parameter
        // types, and constructors.
        for (ClassNode cNode : graph.getClasses())
        {
            if (removeSuper)
            {
                if (cNode.getAssociatedClasses().contains(superClass))
                {
                    cNode.removeAssociatedClass(superClass);
                    graph.remove(graph.getEdge(cNode, superClass, EdgeType.Association));

                    List<AttributeNode> toChange = new ArrayList<AttributeNode>();
                    for (CodeNode node : cNode.getAttributeByType(superClass.getIdentifier()))
                    {
                        if (node instanceof AttributeNode)
                        {
                            toChange.add((AttributeNode) node);
                        }
                    }

                    for (AttributeNode aNode : toChange)
                    {
                        aNode.setType(subClass.getIdentifier());
                        aNode.setAssociatedClass(subClass);
                    }

                    cNode.addAssociatedClass(subClass);
                    if (!graph.isEdge(cNode, subClass, EdgeType.Association))
                        graph.insert(new Edge(cNode, subClass, EdgeType.Association));
                }
            }
            else
            {
                if (cNode.getAssociatedClasses().contains(subClass))
                {
                    cNode.removeAssociatedClass(subClass);
                    graph.remove(graph.getEdge(cNode, subClass, EdgeType.Association));

                    List<AttributeNode> toChange = new ArrayList<AttributeNode>();
                    for (CodeNode node : cNode.getAttributeByType(subClass.getIdentifier()))
                    {
                        if (node instanceof AttributeNode)
                        {
                            toChange.add((AttributeNode) node);
                        }
                    }

                    for (AttributeNode aNode : toChange)
                    {
                        aNode.setType(superClass.getIdentifier());
                        aNode.setAssociatedClass(superClass);
                    }

                    cNode.addAssociatedClass(superClass);
                    if (!graph.isEdge(cNode, superClass, EdgeType.Association))
                        graph.insert(new Edge(cNode, superClass, EdgeType.Association));
                }
            }
        }
    }

    private void removeClasses(CodeGraph graph, ClassNode superClass, ClassNode subClass, boolean removeSuper)
    {
        // 4. Remove the empty class.
        if (removeSuper)
        {
            for (ClassNode cNode : graph.getClasses()) 
            {
                Edge edge = null;
                if (graph.isEdge(cNode, superClass, EdgeType.Association))
                    edge = graph.getEdge(cNode, superClass, EdgeType.Association);
                else if (graph.isEdge(cNode, superClass, EdgeType.Composition))
                    edge = graph.getEdge(cNode, superClass, EdgeType.Composition);
                else
                    edge = graph.getEdge(cNode, superClass, EdgeType.Aggregation);
                
                cNode.removeAssociatedClass(superClass);
                graph.remove(edge);
                
                if (graph.isEdge(superClass, cNode, EdgeType.Association))
                    edge = graph.getEdge(superClass, cNode, EdgeType.Association);
                else if (graph.isEdge(superClass, cNode, EdgeType.Composition))
                    edge = graph.getEdge(superClass, cNode, EdgeType.Composition);
                else
                    edge = graph.getEdge(superClass, cNode, EdgeType.Aggregation);
                
                superClass.removeAssociatedClass(cNode);
                graph.remove(edge);
            }
            graph.remove(superClass);
        }
        else
        {
            for (ClassNode cNode : graph.getClasses()) 
            {
                Edge edge = null;
                if (graph.isEdge(cNode, subClass, EdgeType.Association))
                    edge = graph.getEdge(cNode, subClass, EdgeType.Association);
                else if (graph.isEdge(cNode, subClass, EdgeType.Composition))
                    edge = graph.getEdge(cNode, subClass, EdgeType.Composition);
                else
                    edge = graph.getEdge(cNode, subClass, EdgeType.Aggregation);
                
                cNode.removeAssociatedClass(subClass);
                graph.remove(edge);
                
                if (graph.isEdge(subClass, cNode, EdgeType.Association))
                    edge = graph.getEdge(subClass, cNode, EdgeType.Association);
                else if (graph.isEdge(subClass, cNode, EdgeType.Composition))
                    edge = graph.getEdge(subClass, cNode, EdgeType.Composition);
                else
                    edge = graph.getEdge(subClass, cNode, EdgeType.Aggregation);
                
                superClass.removeAssociatedClass(cNode);
                graph.remove(edge);
            }
            graph.remove(subClass);
        }
    }
}
